﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Gunningine
{
    namespace Rendersystems
    {
        namespace OpenGL
        {
            using OpenTK;
            using OpenTK.Graphics;
            using OpenTK.Graphics.OpenGL;
            using Gunningine;

            public class RenderSystemGL : RenderSystem
            {
                /// <summary>
                /// Constructor.
                /// </summary>
                /// <param name="resourceMgr"></param>
                public RenderSystemGL(ResourceManager resourceMgr)
                    : base(resourceMgr)
                {
                }

                /// <summary>
                /// Initialise OpenGL and so on.
                /// </summary>
                /// <param name="x">x min.</param>
                /// <param name="y">y min.</param>
                /// <param name="width">width.</param>
                /// <param name="height">height.</param>
                public override void Initialise(int x, int y, int width, int height)
                {
                    GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    GL.Color4(1.0f, 1.0f, 1.0f, 1.0f);

                    GL.Enable(EnableCap.DepthTest);
                    GL.Enable(EnableCap.Texture2D);

                    // Blending
                    GL.Enable(EnableCap.Blend);
                    GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);

                    SetDimensions(x, y, width, height);
                }

                /// <summary>
                /// Clear the screen.
                /// </summary>
                public override void ClearScreen()
                {
                    GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
                }

                /// <summary>
                /// Set up viewport and create 3d projection.
                /// </summary>
                /// <param name="fov">horizontal field of view.</param>
                public override void Set3dProjection(float fov)
                {
                    this.Fov = fov;
                    GL.Viewport(this.ScreenLeft, this.ScreenBottom, this.ScreenWidth, this.ScreenHeight);

                    // Calculate aspect ratio and FOV
                    float aspectRatio = this.ScreenWidth / (float)this.ScreenHeight;
                    float fovy = MathsUtils.DegToRad(fov) / aspectRatio;

                    // Create projection matrix and tell OpenGL to use it.
                    Matrix4 proj = Matrix4.CreatePerspectiveFieldOfView(fovy, aspectRatio, 1.0f, 1000.0f);

                    GL.MatrixMode(MatrixMode.Projection);
                    GL.LoadMatrix(ref proj);
                    GL.MatrixMode(MatrixMode.Modelview);

                    ResetTransform();
                }

                /// <summary>
                /// Set up viewport and create 2d projection.
                /// </summary>
                public override void Set2dProjection()
                {
                    GL.Viewport(this.ScreenLeft, this.ScreenBottom, this.ScreenWidth, this.ScreenHeight);

                    // Create projection matrix and tell OpenGL to use it.
                    Matrix4 proj = Matrix4.CreateOrthographic(this.ScreenWidth, this.ScreenHeight, 0.0f, 1.0f);

                    GL.MatrixMode(MatrixMode.Projection);
                    GL.LoadMatrix(ref proj);
                    GL.Translate(-this.ScreenWidth / 2.0, -this.ScreenHeight / 2.0, 0.0);
                    GL.MatrixMode(MatrixMode.Modelview);

                    ResetTransform();
                }

                /// <summary>
                /// Set view based on camera.
                /// </summary>
                /// <param name="camera"></param>
                public override void SetCamera(Camera camera)
                {
                    Gunningine.Vector3 target = camera.Position + camera.Direction;
                    Matrix4 modelView = Matrix4.LookAt(camera.Position.X, camera.Position.Y, camera.Position.Z,
                                                       target.X, target.Y, target.Z,
                                                       camera.Up.X, camera.Up.Y, camera.Up.Z);
                    GL.LoadMatrix(ref modelView);
                }

                /// <summary>
                /// Reset the transform matrix.
                /// </summary>
                public override void ResetTransform()
                {
                    GL.LoadIdentity();
                }

                /// <summary>
                /// Push the current transform.
                /// </summary>
                public override void PushTransform()
                {
                    GL.PushMatrix();
                }

                /// <summary>
                /// Pop the top of the transform stack.
                /// </summary>
                public override void PopTransform()
                {
                    GL.PopMatrix();
                }

                /// <summary>
                /// Translate the current transform.
                /// </summary>
                /// <param name="x">x offset.</param>
                /// <param name="y">y offset.</param>
                /// <param name="z">z offset.</param>
                public override void Translate(float x, float y, float z)
                {
                    GL.Translate(x, y, z);
                }

                /// <summary>
                /// Rotate the current transform.
                /// </summary>
                /// <param name="angle">angle to rotate by.</param>
                /// <param name="x">x-component of rotation axis.</param>
                /// <param name="y">y-component of rotation axis.</param>
                /// <param name="z">z-component of rotation axis.</param>
                public override void Rotate(float angle, float x, float y, float z)
                {
                    GL.Rotate(angle, x, y, z);
                }

                /// <summary>
                /// Scale the current transform.
                /// </summary>
                /// <param name="x">x amount.</param>
                /// <param name="y">y amount.</param>
                /// <param name="z">z amount.</param>
                public override void Scale(float x, float y, float z)
                {
                    GL.Scale(x, y, z);
                }

                /// <summary>
                /// Project a point from world space to screen space.
                /// </summary>
                /// <param name="position"></param>
                /// <param name="camera"></param>
                /// <returns></returns>
                public override Gunningine.Vector2 Project(Gunningine.Vector3 position, Camera camera)
                {
                    Matrix4 modelView, projection;
                    float[] viewport = new float[4];

                    // Set up modelview-projection matrix.
                    GL.MatrixMode(MatrixMode.Projection);
                    GL.PushMatrix();
                    GL.MatrixMode(MatrixMode.Modelview);
                    GL.PushMatrix();

                    this.Set3dProjection(this.Fov);
                    SetCamera(camera);

                    GL.GetFloat(GetPName.ModelviewMatrix, out modelView);
                    GL.GetFloat(GetPName.ProjectionMatrix, out projection);
                    GL.GetFloat(GetPName.Viewport, viewport);

                    // Restore modelview-projection matrix.
                    GL.MatrixMode(MatrixMode.Projection);
                    GL.PopMatrix();
                    GL.MatrixMode(MatrixMode.Modelview);
                    GL.PopMatrix();

                    Matrix4 modelViewProjection = Matrix4.Mult(modelView, projection);

                    Vector4 position3d = new Vector4(position.X, position.Y, position.Z, 1);
                    Vector4 position2d = Vector4.Transform(position3d, modelViewProjection);
                    
                    position2d.X /= position2d.W;
                    position2d.Y /= position2d.W;
                    position2d.Z /= position2d.W;

                    return new Gunningine.Vector2(viewport[0] + viewport[2] * (position2d.X + 1) / 2.0f,
                                                          viewport[1] + viewport[3] * (position2d.Y + 1) / 2.0f);
                }

                /// <summary>
                /// Unproject a point from screen space to a line in world space.
                /// </summary>
                /// <param name="x">x coord.</param>
                /// <param name="y">y coord</param>
                public override Gunningine.Vector3 Unproject(Gunningine.Vector3 position, Camera camera)
                {
                    double[] mm = new double[16];
                    double[] pm = new double[16];
                    int[] viewport = new int[4];

                    // Set up modelview-projection matrix.
                    GL.MatrixMode(MatrixMode.Projection);
                    GL.PushMatrix();
                    GL.MatrixMode(MatrixMode.Modelview);
                    GL.PushMatrix();

                    this.Set3dProjection(this.Fov);
                    SetCamera(camera);

                    GL.GetInteger(GetPName.Viewport, viewport);
                    GL.GetDouble(GetPName.ModelviewMatrix, mm);
                    GL.GetDouble(GetPName.ProjectionMatrix, pm);

                    // Restore modelview-projection matrix.
                    GL.MatrixMode(MatrixMode.Projection);
                    GL.PopMatrix();
                    GL.MatrixMode(MatrixMode.Modelview);
                    GL.PopMatrix();

                    Matrix4 mvMatrix = new Matrix4((float)mm[0], (float)mm[4], (float)mm[8], (float)mm[12],
                                                   (float)mm[1], (float)mm[5], (float)mm[9], (float)mm[13],
                                                   (float)mm[2], (float)mm[6], (float)mm[10], (float)mm[14],
                                                   (float)mm[3], (float)mm[7], (float)mm[11], (float)mm[15]);

                    Matrix4 pjMatrix = new Matrix4((float)pm[0], (float)pm[4], (float)pm[8], (float)pm[12],
                                                   (float)pm[1], (float)pm[5], (float)pm[9], (float)pm[13],
                                                   (float)pm[2], (float)pm[6], (float)pm[10], (float)pm[14],
                                                   (float)pm[3], (float)pm[7], (float)pm[11], (float)pm[15]);

                    Matrix4 modelViewProjection = pjMatrix * mvMatrix;
                    modelViewProjection.Invert();

                    Vector4 fin = new Vector4();

                    fin.X = ((position.X - (float)viewport[0]) / (float)viewport[2]) * 2.0f - 1.0f;
                    fin.Y = ((position.Y - (float)viewport[1]) / (float)viewport[3]) * 2.0f - 1.0f;
                    fin.Z = position.Z;
                    fin.W = 1.0f;

                    return new Gunningine.Vector3(Vector4.Dot(modelViewProjection.Row0, fin), 
                                                          Vector4.Dot(modelViewProjection.Row1, fin), 
                                                          Vector4.Dot(modelViewProjection.Row2, fin));
                }

                /// <summary>
                /// Get clipping planes for the view.  Returns 6 planes in the following order:
                /// Top, bottom, left, right, near, far.
                /// </summary>
                /// <param name="camera"></param>
                /// <returns></returns>
                public override Plane[] GetClippingPlanes(Camera camera)
                {
                    Plane[] planes = new Plane[6];

                    Matrix4 modelView, projection;
                    float[] viewport = new float[4];

                    // Set up modelview-projection matrix.
                    GL.MatrixMode(MatrixMode.Projection);
                    GL.PushMatrix();
                    GL.MatrixMode(MatrixMode.Modelview);
                    GL.PushMatrix();

                    this.Set3dProjection(this.Fov);
                    SetCamera(camera);

                    GL.GetFloat(GetPName.ModelviewMatrix, out modelView);
                    GL.GetFloat(GetPName.ProjectionMatrix, out projection);
                    GL.GetFloat(GetPName.Viewport, viewport);

                    // Restore modelview-projection matrix.
                    GL.MatrixMode(MatrixMode.Projection);
                    GL.PopMatrix();
                    GL.MatrixMode(MatrixMode.Modelview);
                    GL.PopMatrix();

                    // Get modelview-projection matrix.
                    Matrix4 modelViewProjection = Matrix4.Mult(modelView, projection);

                    // Planes
                    Gunningine.Vector3 normal;
                    float distance;

                    // Top
                    normal = new Gunningine.Vector3(
                        modelViewProjection[0, 3] - modelViewProjection[0, 1],
                        modelViewProjection[1, 3] - modelViewProjection[1, 1],
                        modelViewProjection[2, 3] - modelViewProjection[2, 1]);
                    distance = (modelViewProjection[3, 3] - modelViewProjection[3, 1]) / normal.Length;
                    planes[0] = new Plane(normal.Normalised(), distance);

                    // Bottom
                    normal = new Gunningine.Vector3(
                        modelViewProjection[0, 3] + modelViewProjection[0, 1],
                        modelViewProjection[1, 3] + modelViewProjection[1, 1],
                        modelViewProjection[2, 3] + modelViewProjection[2, 1]);
                    distance = (modelViewProjection[3, 3] + modelViewProjection[3, 1]) / normal.Length;
                    planes[1] = new Plane(normal.Normalised(), distance);

                    // Left
                    normal = new Gunningine.Vector3(
                        modelViewProjection[0, 3] + modelViewProjection[0, 0],
                        modelViewProjection[1, 3] + modelViewProjection[1, 0],
                        modelViewProjection[2, 3] + modelViewProjection[2, 0]);
                    distance = (modelViewProjection[3, 3] + modelViewProjection[3, 0]) / normal.Length;
                    planes[2] = new Plane(normal.Normalised(), distance);

                    // Right
                    normal = new Gunningine.Vector3(
                        modelViewProjection[0, 3] - modelViewProjection[0, 0],
                        modelViewProjection[1, 3] - modelViewProjection[1, 0],
                        modelViewProjection[2, 3] - modelViewProjection[2, 0]);
                    distance = (modelViewProjection[3, 3] - modelViewProjection[3, 0]) / normal.Length;
                    planes[3] = new Plane(normal.Normalised(), distance);

                    // Near
                    normal = new Gunningine.Vector3(
                        modelViewProjection[0, 3] + modelViewProjection[0, 2],
                        modelViewProjection[1, 3] + modelViewProjection[1, 2],
                        modelViewProjection[2, 3] + modelViewProjection[2, 2]);
                    distance = (modelViewProjection[3, 3] + modelViewProjection[3, 2]) / normal.Length;
                    planes[4] = new Plane(normal.Normalised(), distance);

                    // Far
                    normal = new Gunningine.Vector3(
                        modelViewProjection[0, 3] - modelViewProjection[0, 2],
                        modelViewProjection[1, 3] - modelViewProjection[1, 2],
                        modelViewProjection[2, 3] - modelViewProjection[2, 2]);
                    distance = (modelViewProjection[3, 3] - modelViewProjection[3, 2]) / normal.Length;
                    planes[5] = new Plane(normal.Normalised(), distance);

                    return planes;
                }

                /// <summary>
                /// Create a new shader.
                /// </summary>
                /// <param name="vertexShader">vertex shader.</param>
                /// <param name="fragmentShader">fragment shader.</param>
                /// <returns></returns>
                protected override Shader CreateShaderImpl(TextResource vertexShader, TextResource fragmentShader)
                {
                    return new ShaderGL(vertexShader, fragmentShader);
                }

                /// <summary>
                /// Create a new texture.
                /// </summary>
                /// <param name="width"></param>
                /// <param name="height"></param>
                /// <param name="channels"></param>
                /// <param name="data"></param>
                /// <returns></returns>
                public override Texture CreateTextureImpl(int width, int height, int channels, IntPtr data)
                {
                    return new TextureGL(width, height, channels, data);
                }

                /// <summary>
                /// Create a new render texture.
                /// </summary>
                /// <param name="width"></param>
                /// <param name="height"></param>
                /// <param name="channels"></param>
                /// <returns></returns>
                protected override RenderTexture CreateRenderTextureImpl(int width, int height, int channels)
                {
                    return new RenderTextureGL(width, height, channels, this);
                }

                /// Create new mesh.
                /// </summary>
                /// <param name="model"></param>
                /// <returns></returns>
                protected override Mesh CreateMeshImpl(ModelResource model)
                {
                    return new MeshGL(model);
                }

                /// <summary>
                /// Set colour.
                /// </summary>
                /// <param name="r">red component [0-1].</param>
                /// <param name="g">green component [0-1].</param>
                /// <param name="b">blue component [0-1].</param>
                /// <param name="a">alpha component [0-1].</param>
                public override void SetColour(float r, float g, float b, float a = 1.0f)
                {
                    GL.Color4(r, g, b, a);
                }

                /// <summary>
                /// Print text at the specified location.
                /// </summary>
                /// <param name="x">x position.</param>
                /// <param name="y">y position, from top ofscreen.</param>
                /// <param name="font">font to use.</param>
                /// <param name="text">text to print.</param>
                public override void Print(int x, int y, string font, string text)
                {
                    FontResource fr = (FontResource)this.resourceMgr.GetResource(font);
                    Texture texture = GetTexture(fr.Image);

                    float imageWidth = texture.Width;
                    float imageHeight = texture.Height;

                    int xpos = x;
                    int ypos = this.ScreenHeight - fr.Size - y;

                    texture.Bind();
                    GL.Begin(PrimitiveType.Quads);
                    for (int i = 0; i < text.Length; ++i)
                    {
                        char chr = text[i];
                        FontResource.Glyph g = fr.Glyphs[(int)chr];

                        GL.TexCoord2(g.x / imageWidth, (g.y + g.h) / imageHeight);
                        GL.Vertex2(xpos, ypos);
                        GL.TexCoord2((g.x + g.w) / imageWidth, (g.y + g.h) / imageHeight);
                        GL.Vertex2(xpos + g.w, ypos);
                        GL.TexCoord2((g.x + g.w) / imageWidth, g.y / imageHeight);
                        GL.Vertex2(xpos + g.w, ypos + g.h);
                        GL.TexCoord2(g.x / imageWidth, g.y / imageHeight);
                        GL.Vertex2(xpos, ypos + g.h);

                        xpos += (g.w + g.kern);
                    }
                    GL.End();

                    texture.Unbind();
                }

                /// <summary>
                /// Render a quad.
                /// </summary>
                /// <param name="p0">pos coord 1</param>
                /// <param name="p1">pos coord 2</param>
                /// <param name="p2">pos coord 3</param>
                /// <param name="p3">pos coord 4</param>
                public override void RenderQuad3d(Gunningine.Vector3 p0, Gunningine.Vector3 p1, Gunningine.Vector3 p2, Gunningine.Vector3 p3)
                {
                    GL.Begin(PrimitiveType.Quads);
                    GL.Vertex3(p0.X, p0.Y, p0.Z);
                    GL.Vertex3(p1.X, p1.Y, p1.Z);
                    GL.Vertex3(p2.X, p2.Y, p2.Z);
                    GL.Vertex3(p3.X, p3.Y, p3.Z);
                    GL.End();
                }

                /// <summary>
                /// Render a textured quad.
                /// </summary>
                /// <param name="p0">pos coord 1</param>
                /// <param name="uv0">uv-coord 1</param>
                /// <param name="p1">pos coord 2</param>
                /// <param name="uv1">uv-coord 2</param>
                /// <param name="p2">pos coord 3</param>
                /// <param name="uv2">uv-coord 3</param>
                /// <param name="p3">pos coord 4</param>
                /// <param name="uv3">uv-coord 4</param>
                public override void RenderTexturedQuad3d(Gunningine.Vector3 p0, Gunningine.Vector2 uv0,
                                                          Gunningine.Vector3 p1, Gunningine.Vector2 uv1,
                                                          Gunningine.Vector3 p2, Gunningine.Vector2 uv2,
                                                          Gunningine.Vector3 p3, Gunningine.Vector2 uv3)
                {
                    GL.Begin(PrimitiveType.Quads);
                    GL.TexCoord2(uv0.X, uv0.Y);
                    GL.Vertex3(p0.X, p0.Y, p0.Z);
                    GL.TexCoord2(uv1.X, uv1.Y);
                    GL.Vertex3(p1.X, p1.Y, p1.Z);
                    GL.TexCoord2(uv2.X, uv2.Y);
                    GL.Vertex3(p2.X, p2.Y, p2.Z);
                    GL.TexCoord2(uv3.X, uv3.Y);
                    GL.Vertex3(p3.X, p3.Y, p3.Z);
                    GL.End();
                }

                /// <summary>
                /// Render a quad.
                /// </summary>
                /// <param name="p0">pos coord 1</param>
                /// <param name="p1">pos coord 2</param>
                /// <param name="p2">pos coord 3</param>
                /// <param name="p3">pos coord 4</param>
                public override void RenderQuad2d(Gunningine.Vector2 p0, Gunningine.Vector2 p1,
                                                  Gunningine.Vector2 p2, Gunningine.Vector2 p3)
                {
                    GL.Begin(PrimitiveType.Quads);
                    GL.Vertex2(p0.X, p0.Y);
                    GL.Vertex2(p1.X, p1.Y);
                    GL.Vertex2(p2.X, p2.Y);
                    GL.Vertex2(p3.X, p3.Y);
                    GL.End();
                }

                /// <summary>
                /// Render a set of lines, specified as (x0,y0,x1,y1) groups.
                /// </summary>
                /// <param name="points"></param>
                public override void RenderLineList2d(List<float> points)
                {
                    GL.Begin(PrimitiveType.Lines);
                    for (int i = 0; i < points.Count; i += 4)
                    {
                        GL.Vertex2(points[i + 0], points[i + 1]);
                        GL.Vertex2(points[i + 2], points[i + 3]);
                    }
                    GL.End();
                }

                /// <summary>
                /// Render a textured quad.
                /// </summary>
                /// <param name="p0">pos coord 1</param>
                /// <param name="uv0">uv-coord 1</param>
                /// <param name="p1">pos coord 2</param>
                /// <param name="uv1">uv-coord 2</param>
                /// <param name="p2">pos coord 3</param>
                /// <param name="uv2">uv-coord 3</param>
                /// <param name="p3">pos coord 4</param>
                /// <param name="uv3">uv-coord 4</param>
                public override void RenderTexturedQuad2d(Gunningine.Vector2 p0, Gunningine.Vector2 uv0,
                                                          Gunningine.Vector2 p1, Gunningine.Vector2 uv1,
                                                          Gunningine.Vector2 p2, Gunningine.Vector2 uv2,
                                                          Gunningine.Vector2 p3, Gunningine.Vector2 uv3)
                {
                    GL.Begin(PrimitiveType.Quads);
                    GL.TexCoord2(uv0.X, uv0.Y);
                    GL.Vertex2(p0.X, p0.Y);
                    GL.TexCoord2(uv1.X, uv1.Y);
                    GL.Vertex2(p1.X, p1.Y);
                    GL.TexCoord2(uv2.X, uv2.Y);
                    GL.Vertex2(p2.X, p2.Y);
                    GL.TexCoord2(uv3.X, uv3.Y);
                    GL.Vertex2(p3.X, p3.Y);
                    GL.End();
                }

                /// <summary>
                /// Render a cube of half-width 'size'.
                /// </summary>
                /// <param name="size">half-width of cube.</param>
                public override void RenderPlaceHolderEntity(float size, float r, float g, float b)
                {
                    GL.Begin(PrimitiveType.Quads);

                    GL.Color4(r, g, b, 1);

                    // Back
                    GL.Vertex3(-size, -size, size);
                    GL.Vertex3(size, -size, size);
                    GL.Vertex3(size, size, size);
                    GL.Vertex3(-size, size, size);

                    GL.Color4(r * 0.7f, g * 0.7f, b * 0.7f, 1);

                    // Up
                    GL.Vertex3(-size, size, -size);
                    GL.Vertex3(size, size, -size);
                    GL.Vertex3(size, size, size);
                    GL.Vertex3(-size, size, size);

                    // Down
                    GL.Vertex3(-size, -size, -size);
                    GL.Vertex3(size, -size, -size);
                    GL.Vertex3(size, -size, size);
                    GL.Vertex3(-size, -size, size);

                    // Left
                    GL.Vertex3(-size, -size, -size);
                    GL.Vertex3(-size, size, -size);
                    GL.Vertex3(-size, size, size);
                    GL.Vertex3(-size, -size, size);

                    // Right
                    GL.Vertex3(size, -size, -size);
                    GL.Vertex3(size, size, -size);
                    GL.Vertex3(size, size, size);
                    GL.Vertex3(size, -size, size);

                    GL.End();
                }
            }
        }
    }
}
